/** SocketManager */
package server;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.json.JSONArray;
import org.json.JSONObject;

import server.service.appointment.AppointmentModel;
import server.service.doctor.DoctorModel;
import server.service.medicalservice.ServiceModel;
import server.service.nurse.NurseModel;
import server.service.patient.PatientModel;
import server.service.user.UserModel;
import domain.action.Action;
import domain.admin.Admin;
import domain.appointment.Appointment;
import domain.doctor.Doctor;
import domain.exception.ValidationException;
import domain.nurse.Nurse;
import domain.patient.Patient;
import domain.permissions.Permission;
import domain.records.MedicalService;
import domain.session.Request;
import domain.session.Response;
import domain.session.UserSession;
import domain.session.UserSessionThreadLocal;
import domain.user.SystemUser;
import domain.user.User;

/**
 * SocketManager manages individual sockets concurrently as they make requests.
 * @author jessienewman
 * @version $Revision: 1.0 $
 */
public class SocketManager implements Runnable {
	/**
	 * Field LOGGER.
	 */
	private static final Logger LOGGER = Logger.getLogger(SocketManager.class.getName());

	/**
	 * Field socket.
	 */
	private Socket socket;

	/**
	 * Field out.
	 */
	private Writer out;
	
	/**
	 * Field in.
	 */
	private Reader in;
	
	/**
	 * Field SESSIONId.
	 */
	private Integer sessionId = null;
	
	/**
	 * Field userId.
	 */
	private Integer userId;

	/**
	 * Field userModel.
	 */
	private UserModel userModel;
	
	/**
	 * Field patientModel.
	 */
	private PatientModel patientModel;

	/**
	 * Field invalidLogins.
	 */
	private Map<String, Integer> invalidLogins;

	/**
	 * Field loginSuccess.
	 */
	private boolean loginSuccess;
	
	private DoctorModel doctorModel;
	
	private AppointmentModel apptModel;
	
	private NurseModel nurseModel;
	
	private ServiceModel serviceModel;

	/**
	 * Creates a SocketManager for the given socket.
	 * @param socket
	 */
	public SocketManager(final Socket socket) {
		this.socket = socket;
		userModel = new UserModel();
		invalidLogins = new HashMap<String, Integer>();
		loginSuccess = false;
		patientModel = new PatientModel();
		doctorModel = new DoctorModel();
		apptModel = new AppointmentModel();
		nurseModel = new NurseModel();
		serviceModel = new ServiceModel();
	}

	/**
	 * Runs the SocketManager thread.
	 * @see java.lang.Runnable#run()
	 */
	public void run() {

		try {
			//Retrieve the socket's streams
			out = new BufferedWriter(new OutputStreamWriter(
					socket.getOutputStream(), "ASCII"));
			in = new InputStreamReader(new BufferedInputStream(socket.getInputStream()));

			//Loop until the user has logged in
			while (!loginSuccess) {
				//Read the request
				Request request = readRequest();

				//Client connection was closed
				if (request == null) {
					return;
				}

				//Process the Request and write the response
				Response response = processRequest(request);

				//Write the Response
				writeResponse(response);
			}

			//Loop indefinitely while requests are still coming in
			boolean infiniteLoop = true;
			while (infiniteLoop) {
				//Read the request
				Request request = readRequest();

				//Client connection was closed
				if (request == null) {
					return;
				}

				//Process the Request
				Response response = processRequest(request);

				//Write the Response
				writeResponse(response);
			}
		} catch (final IOException ex) {
			LOGGER.warning(ex.getMessage());
		}
	}

	/**
	 * Processes a given Request based off of the Action of the request and
	 * then returns the appropriate Response.
	 * @param request
	
	 * @return Response */
	private Response processRequest(final Request request) {
		Response response = null;
		JSONObject responseJson = null;
		JSONObject requestJson = request.getRequest();
		Action action = request.getAction();

		//Make sure that the request is for a login action if there is no SESSION
		if (sessionId == null && !Action.LOGIN.equals(action)) {
			responseJson.put("msg", "User is not logged in!");
			response = new Response(false, responseJson);
			return response;
		}

		//Perform a different task depending on the Action
		switch (action) {
		case LOGIN:
			responseJson = attemptLogin(requestJson);
			response = new Response(responseJson);
			break;
		case GET_PATIENT:
			try {
				Integer userId = requestJson.getInt("patientId");
				SystemUser user = (SystemUser) userModel.getUser(userId);
				response = new Response(true, user.getJSON());
			} catch (NumberFormatException nfe) {
				LOGGER.warning("Number Format Exception: " + nfe);
			}
			break;
		case FIND_PATIENTS:
			String query = requestJson.getString("query");
			List<Patient> patients = patientModel.findPatients(query);
			JSONArray patientArray = new JSONArray();
			for (Patient patient : patients) {
				patientArray.put(patient.getJSON());
			}
			responseJson = new JSONObject();
			responseJson.put("patients", patientArray);
			response = new Response(true, responseJson);
			break;
		case GET_ALL_PATIENTS:
			List<Patient> allPatients = patientModel.getAllPatients();
			JSONArray allPatientArray = new JSONArray();
			for (Patient patient : allPatients) {
				allPatientArray.put(patient.getJSON());
			}
			responseJson = new JSONObject();
			responseJson.put("patients", allPatientArray);
			response = new Response(true, responseJson);
			break;
		case MODIFY_DOB:
			Integer userId = requestJson.getInt("userId");
			String date = requestJson.getString("date");
			response = new Response();
			try {
				patientModel.modifyDOB(userId, date);
			} catch (ValidationException e) {
				responseJson = new JSONObject();
				responseJson.put("msg", e.getExceptionText());
				response.setJSON(responseJson);
				response.setSuccessful(false);
				break;
			}
			response.setSuccessful(true);
			break;
		case MODIFY_ALLERGIES:
			Integer id = requestJson.getInt("userId");
			String allergies = requestJson.getString("allergies");
			patientModel.modifyAllergies(id, allergies);
			response = new Response();
			response.setSuccessful(true);
			break;
		case MODIFY_INSURANCE:
			Integer insuranceId = requestJson.getInt("userId");
			String insurance = requestJson.getString("insurance");
			patientModel.modifyInsurance(insuranceId, insurance);
			response = new Response();
			response.setSuccessful(true);
			break;
		case MODIFY_CONTACT:
			Integer contactId = requestJson.getInt("userId");
			String address = requestJson.getString("address");
			String email = requestJson.getString("email");
			response = new Response();
			try {
				patientModel.modifyContact(contactId, address, email);
			} catch (ValidationException e) {
				responseJson = new JSONObject();
				responseJson.put("msg", e.getExceptionText());
				response.setJSON(responseJson);
				response.setSuccessful(false);
				break;
			}
			response.setSuccessful(true);
			break;
		case FIND_USERS:
			String userQuery = requestJson.getString("query");
			List<User> userList = userModel.findUsers(userQuery);
			JSONArray userArray = new JSONArray();
			
			for (User user : userList) {
				userArray.put(user);
			}
			
			responseJson = new JSONObject();
			responseJson.put("users", userArray);
			response = new Response(true, responseJson);
			break;
		case GET_USER_ID:
			String username = requestJson.getString("username");
			Integer uid = userModel.getUserId(username);
			responseJson = new JSONObject();
			responseJson.put("userId", uid);
			response = new Response(true, responseJson);
			break;
		case IS_VALID_USERNAME:
			String name = requestJson.getString("username");
			boolean valid = userModel.thisIsValidUsername(name);
			response = new Response();
			response.setSuccessful(valid);
			break;
		case GET_USER:
			Integer getUserId = requestJson.getInt("userId");
			User user = userModel.getUser(getUserId);
			response = new Response(true, ((SystemUser)user).getJSON());
			break;
		case MODIFY_NAME:
			Integer modUserId = requestJson.getInt("userId");
			String nameFirst = requestJson.getString("nameFirst");
			String nameMiddle = requestJson.getString("nameMiddle");
			String nameLast = requestJson.getString("nameLast");
			response = new Response();
			try {
				userModel.modifyName(modUserId, nameFirst, nameMiddle, nameLast);
			} catch (ValidationException e) {
				responseJson = new JSONObject();
				responseJson.put("msg", e.getExceptionText());
				response.setJSON(responseJson);
				response.setSuccessful(false);
				break;
			}
			response.setSuccessful(true);
			break;
		case SET_ACTIVE:
			Integer activeId = requestJson.getInt("userId");
			Boolean isActive = requestJson.getBoolean("active");
			userModel.setActive(activeId, isActive);
			response = new Response();
			response.setSuccessful(true);
			break;
		case ADD_USER:
			SystemUser u = new SystemUser(requestJson.getJSONObject("user"));
			String uname = requestJson.getString("username");
			String p1 = requestJson.getString("p1");
			String p2 = requestJson.getString("p2");
			Integer uId = 0;
			response = new Response();
			try {
				uId = userModel.addUser(u, uname, p1, p2);
			} catch (ValidationException e) {
				responseJson = new JSONObject();
				responseJson.put("msg", e.getExceptionText());
				response.setJSON(responseJson);
				response.setSuccessful(false);
				break;
			}
			response.setSuccessful(true);
			responseJson = new JSONObject();
			responseJson.put("userId", uId);
			response.setJSON(responseJson);
			break;
		case GET_DOCTOR:
			Integer docId = requestJson.getInt("userId");
			Doctor doc = doctorModel.getDoctor(docId);
			response = new Response(true, doc.getJSON());
			break;
		case GET_ALL_DOCTORS:
			List<Doctor> doctors = doctorModel.getAllDoctors();
			JSONArray allDoctorsArray = new JSONArray();
			for (Doctor doctor : doctors) {
				allDoctorsArray.put(doctor.getJSON());
			}
			responseJson = new JSONObject();
			responseJson.put("doctors", allDoctorsArray);
			response = new Response(true, responseJson);
			break;
		case ADD_APPOINTMENT:
			Appointment appt = new Appointment(requestJson.getJSONObject("appointment"));
			apptModel.addAppointment(appt);
			response = new Response();
			response.setSuccessful(true);
			break;
		case REMOVE_APPOINTMENT:
			Appointment rappt = new Appointment(requestJson.getJSONObject("appointment"));
			apptModel.remove(rappt);
			response = new Response();
			response.setSuccessful(true);
			break;
		case GET_PATIENT_APPOINTMENTS:
			Integer pid = requestJson.getInt("userId");
			List<Appointment> pAppts = apptModel.getPatientAppointments(pid);
			JSONArray pApptArray = new JSONArray();
			
			for (Appointment a : pAppts) {
				pApptArray.put(a.getJSON());
			}
			responseJson = new JSONObject();
			responseJson.put("appointments", pApptArray);
			response = new Response(true, responseJson);
			break;
		case GET_DOCTOR_APPOINTMENTS:
			Integer did = requestJson.getInt("userId");
			List<Appointment> dAppts = apptModel.getDoctorAppointments(did);
			JSONArray dApptArray = new JSONArray();
			
			for (Appointment a : dAppts) {
				dApptArray.put(a.getJSON());
			}
			responseJson = new JSONObject();
			responseJson.put("appointments", dApptArray);
			response = new Response(true, responseJson);
			break;
		case GET_LAST_MONTH_DOCTOR_APPOINTMENTS:
			Integer docid = requestJson.getInt("userId");
			List<Appointment> docAppts = apptModel.getLastMonthDoctorAppointments(docid);
			JSONArray docApptArray = new JSONArray();
			
			for (Appointment a : docAppts) {
				docApptArray.put(a.getJSON());
			}
			responseJson = new JSONObject();
			responseJson.put("appointments", docApptArray);
			response = new Response(true, responseJson);
			break;
		case GET_FUTURE_DOCTOR_APPOINTMENTS:
			Integer fDocid = requestJson.getInt("userId");
			List<Appointment> fDocAppts = apptModel.getLastMonthDoctorAppointments(fDocid);
			JSONArray fDocApptArray = new JSONArray();
			
			for (Appointment a : fDocAppts) {
				fDocApptArray.put(a.getJSON());
			}
			responseJson = new JSONObject();
			responseJson.put("appointments", fDocApptArray);
			response = new Response(true, responseJson);
			break;
		case GET_FUTURE_PATIENT_APPOINTMENTS:
			Integer fPid = requestJson.getInt("userId");
			List<Appointment> fPAppts = apptModel.getLastMonthDoctorAppointments(fPid);
			JSONArray fPApptArray = new JSONArray();
			
			for (Appointment a : fPAppts) {
				fPApptArray.put(a.getJSON());
			}
			responseJson = new JSONObject();
			responseJson.put("appointments", fPApptArray);
			response = new Response(true, responseJson);
			break;
		case SAVE_APPOINTMENT:
			apptModel.save();
			response = new Response();
			response.setSuccessful(true);
			break;
		case GET_ALL_NURSES:
			List<Nurse> nurses = nurseModel.getAllNurses();
			JSONArray nursesArray = new JSONArray();
			for (Nurse nurse : nurses) {
				nursesArray.put(nurse.getJSON());
			}
			responseJson = new JSONObject();
			responseJson.put("nurses", nursesArray);
			response = new Response(true, responseJson);
			break;
		case GET_ALL_SERVICES:
			List<MedicalService> services = serviceModel.getAllServices();
			JSONArray servicesArray = new JSONArray();
			for (MedicalService service : services) {
				servicesArray.put(service.getJSON());
			}
			responseJson = new JSONObject();
			responseJson.put("services", servicesArray);
			response = new Response(true, responseJson);
			break;
		default:
			break;
		}

		return response;
	}

	/**
	 * Attempt to login using the credentials stored in the given
	 * JSONObject.
	 * @param json
	
	 * @return JSONObject */
	private JSONObject attemptLogin(final JSONObject json) {
		//The JSON for the response
		JSONObject loginJson = new JSONObject();

		//Retrieve the username and password
		String userName = json.getString("username");
		String password = json.getString("password");

		//Check if the username is valid
		boolean validUser = userModel.thisIsValidUsername(userName);

		//Check if the credentials are correct
		loginSuccess = userModel.loginValid(userName, password);

		//Put the outcome in the json
		loginJson.put("success", loginSuccess);

		String msg = "";
		loginJson.put("lock", false);

		if (!validUser) {
			msg = "The username provided does not exist. Please try again.";
		} else {
			if (!loginSuccess) {
				//Update the amount of invalid logins
				if (invalidLogins.containsKey(userName)) {
					invalidLogins.put(userName, invalidLogins.get(userName) + 1);
				} else {
					invalidLogins.put(userName, 1);
				}

				//Lock the user out if invalid logins has been exceeded
				if (invalidLogins.get(userName) > 3) { // $codepro.audit.disable numericLiterals
					msg = "Too many invalid logins. Contact an administrator.";
					loginJson.put("lock", true);
				} else {
					msg = "Invalid login information. Please try again.";
				}
				LOGGER.info("Login attempt number " + invalidLogins.get(userName) + 
						" for " + userName + " failed");
			} else {
				msg = "Login successful!";

				//Get the logged in user
				userId = userModel.getUserId(userName);
				User user = userModel.getUser(userId);

				//Put the userId in the response
				loginJson.put("userId", userId);
				LOGGER.info("User logged in: " + userName + " (" + userId + ")");

				//Place the user's permissions into the response
				JSONArray array = new JSONArray();
				List<Permission> permissions = getPermissions(user);

				for (Permission p : permissions) {
					array.put(p.toString());
				}

				loginJson.put("permissions", array);

				try {
					//Add the new SESSION
					sessionId = SessionManager.addSession(userId);
					UserSessionThreadLocal.set(new UserSession(userId, sessionId));
					UserSessionThreadLocal.get().setPermissions(permissions);
				} catch (IOException ex) {
					msg = "Too many users are already logged in!";
					LOGGER.info(msg + " : " + ex);
				}
			}
		}

		loginJson.put("msg", msg);

		return loginJson;
	}

	/**
	 * Creates a List of Permissions for the given User.
	 * @param user
	
	 * @return List<Permission> */
	private List<Permission> getPermissions(final User user) {
		List<Permission> permissions = new ArrayList<Permission>();

		//Assign Permissions based off of the user's role
		if (user instanceof Doctor) {
			permissions.add(Permission.DOCTORS_ORDER);
			permissions.add(Permission.INVOICE);
			permissions.add(Permission.DOCTOR);
		}

		if (user instanceof Nurse || user instanceof Doctor || user instanceof Admin) {
			permissions.add(Permission.EDIT_PATIENT);
		}

		if (user instanceof Admin) {
			permissions.add(Permission.ADMIN);
		}
		
		if (user instanceof Patient) {
			permissions.add(Permission.PATIENT);
		}

		return permissions;
	}

	/**
	 * Read an incoming request from the socket's input stream
	 * and return the populated Request object once reading from 
	 * the stream has been completed.
	 * @return Request 
	 * @throws IOException */
	private Request readRequest() throws IOException {
		//Read the request
		String requestStr = "";
		boolean infiniteLoop = true;
		while(infiniteLoop) {
			int c = in.read();
			if (c == '\r' || c == '\n') break;
			if (c == -1) {
				close();
				return null;
			}
			requestStr += (char) c;
		}

		//Create a new Request with what was read
		Request request = new Request(requestStr);
		LOGGER.info("Got Request: " + request.getJSON());

		return request;
	}

	/**
	 * Writes a given Response to the socket's ouput stream.
	 * @param response
	
	 * @throws IOException */
	private void writeResponse(final Response response) throws IOException {
		out.write(response.toString() + "\r");
		out.flush();
	}

	/**
	 * Performs the actions necessary to close the socket.
	 */
	private void close() {
		if (socket != null) {
			try {
				//Remove the open SESSION
				if (sessionId != null) {
					SessionManager.removeSession(sessionId);
				}

				//Close the socket
				socket.close();
				LOGGER.info("Socket closed");
			} catch (final IOException e) { // $codepro.audit.disable logExceptions
				LOGGER.warning("Could not close socket - Session ID: " + sessionId);
			}
		}
	}
	
	/**
	 * super's toString
	
	 * @return super's toString */
	public String toString() {
		return super.toString();
	}
}
